<# .SYNOPSIS Configure Accounts security options using positional arguments - Secpol Policy Framework. .SCRIPTTYPE Computer Configuration .DESCRIPTION This script applies Accounts-related security option settings using positional arguments. Configures the following policies (in alphabetical order): 1. Accounts: Administrator account status (Enable/Disable built-in Administrator) 2. Accounts: Block Microsoft accounts (0=Disabled,1=Enabled,2=Limited,3=Block all Microsoft accounts) 3. Accounts: Guest account status (Enable/Disable built-in Guest account) 4. Accounts: Limit local account use of blank passwords to console logon only (0=Disabled, 1=Enabled) 5. Accounts: Rename administrator account (New name for Administrator account) 6. Accounts: Rename guest account (New name for Guest account) Arguments are provided in the order listed above. Empty/missing arguments skip that policy. .PARAMETER PolicyValues JSON array string containing policy values (in order). Use empty strings "" to skip policies. Format: '["value1","value2","value3",...]' .PARAMETER LogLevel Logging verbosity: Silent, Normal, Verbose, Debug .PARAMETER LogPath Custom log file path (optional) .PARAMETER WhatIf Preview changes without applying them .EXAMPLE .\Set-AccountsSecurityOptions.ps1 '["0","3","0","1","Admin","Visitor"]' Disables Administrator account, blocks Microsoft accounts, disables Guest, enables blank password restriction, renames Administrator to "Admin" and Guest to "Visitor" .EXAMPLE .\Set-AccountsSecurityOptions.ps1 '["0","3","0","1","",""]' -WhatIf Preview changes for first 4 policies, skip rename policies #> param( [Parameter(Position=0, ValueFromRemainingArguments=$true)] [string[]]$PolicyValuesArray = @("[]"), [ValidateSet('Silent','Normal','Verbose','Debug')] [string]$LogLevel = 'Normal', [string]$LogPath = $null, [switch]$WhatIf ) # Combine all arguments into a single PolicyValues string # First, try to get the original command line with proper quotes $PolicyValues = $null try { $currentPID = $PID Write-Host "Current Process ID: $currentPID" -ForegroundColor Cyan $process = Get-CimInstance Win32_Process -Filter "ProcessId = $currentPID" if ($process) { $commandLine = $process.CommandLine Write-Host "Full command line: $commandLine" -ForegroundColor Yellow if ($commandLine) { # Get the script name for more precise regex matching $scriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Path) $escapedScriptName = [regex]::Escape($scriptName) # Extract the first argument after this specific script (with all quotes intact) # Stop at known parameters: -LogLevel, -LogPath, -WhatIf, or end of string $pattern = "-File\s+`"[^`"]*\\$escapedScriptName`"\s+(.+?)(?:\s+(?:-LogLevel|-LogPath|-WhatIf)|$)" Write-Host "Using regex pattern: $pattern" -ForegroundColor DarkGray if ($commandLine -match $pattern) { $rawArgument = $matches[1].Trim() Write-Host "Raw argument extracted: $rawArgument" -ForegroundColor Magenta # Remove outer quotes if present if ($rawArgument -match '^"(.*)"$') { $PolicyValues = $matches[1] } else { $PolicyValues = $rawArgument } Write-Host "Extracted PolicyValues from command line: $PolicyValues" -ForegroundColor Green } else { Write-Host "Command line regex did not match. Command line: $commandLine" -ForegroundColor Red } } else { Write-Host "CommandLine property is null or empty" -ForegroundColor Red } } else { Write-Host "Failed to get process information for PID $currentPID" -ForegroundColor Red } } catch { Write-Host "Error extracting from command line: $($_.Exception.Message)" -ForegroundColor Red Write-Verbose "Could not extract from command line: $($_.Exception.Message)" } # Fallback: Use parameter-based approach if command line extraction failed if (-not $PolicyValues) { $PolicyValues = if ($PolicyValuesArray.Count -gt 1) { # Multiple arguments - join them back together $PolicyValuesArray -join '' } else { # Single argument - use as-is $PolicyValuesArray[0] } Write-Verbose "Using parameter-based PolicyValues: $PolicyValues" } # Input DB - Accounts Security Options (Alphabetically ordered) # RegistryType: 4=DWORD, 1=String, 7=MultiString, 3=Binary $PolicyDatabase = @( @{ Name = "Accounts: Administrator account status" KeyGroup = "[System Access]" Key = "EnableAdminAccount" }, @{ Name = "Accounts: Block Microsoft accounts" KeyGroup = "[Registry Values]" Key = "MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\NoConnectedUser" RegistryType = 4 }, @{ Name = "Accounts: Guest account status" KeyGroup = "[System Access]" Key = "EnableGuestAccount" }, @{ Name = "Accounts: Limit local account use of blank passwords to console logon only" KeyGroup = "[Registry Values]" Key = "MACHINE\System\CurrentControlSet\Control\Lsa\LimitBlankPasswordUse" RegistryType = 4 }, @{ Name = "Accounts: Rename administrator account" KeyGroup = "[System Access]" Key = "NewAdministratorName" }, @{ Name = "Accounts: Rename guest account" KeyGroup = "[System Access]" Key = "NewGuestName" } ) # Script-wide variables $script:LogFile = $null $script:StartTime = Get-Date $script:ProcessedCount = 0 $script:SuccessCount = 0 $script:FailureCount = 0 $script:SkippedCount = 0 # Initialize logging function Initialize-LogPath { if ($LogPath) { $logDir = Split-Path $LogPath -Parent if ($logDir -and -not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } return $LogPath } # Try to get agent directory, fallback to script directory $baseDir = $PSScriptRoot try { $registryPath = if ([Environment]::Is64BitOperatingSystem) { "HKLM:\SOFTWARE\WOW6432Node\AdventNet\DesktopCentral\DCAgent" } else { "HKLM:\SOFTWARE\AdventNet\DesktopCentral\DCAgent" } $agentDir = Get-ItemProperty -Path $registryPath -Name "DCAgentInstallDir" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DCAgentInstallDir if ($agentDir -and (Test-Path $agentDir)) { $baseDir = $agentDir } } catch { # Silently fall back to script directory } $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $logDir = Join-Path (Join-Path $baseDir "logs") "SecurityPolicies" if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } return Join-Path $logDir "AccountsSecurityOptions_$timestamp.log" } # Logging function with log levels function Write-Log { param( [string]$Message, [ValidateSet('INFO','SUCCESS','WARNING','ERROR','DEBUG','PROGRESS')] [string]$Level = 'INFO', [string]$Component = 'Main' ) $levelPriority = @{ 'Silent' = 0 'Normal' = 1 'Verbose' = 2 'Debug' = 3 } $messagePriority = @{ 'ERROR' = 0 'WARNING' = 0 'SUCCESS' = 1 'PROGRESS' = 1 'INFO' = 2 'DEBUG' = 3 } $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $logEntry = "[$timestamp] [$Level] [$Component] $Message" # Always write to log file if ($script:LogFile) { Add-Content -Path $script:LogFile -Value $logEntry -ErrorAction SilentlyContinue } # Console output based on log level if ($levelPriority[$LogLevel] -ge $messagePriority[$Level]) { switch ($Level) { 'ERROR' { Write-Host $logEntry -ForegroundColor Red } 'WARNING' { Write-Host $logEntry -ForegroundColor Yellow } 'SUCCESS' { Write-Host $logEntry -ForegroundColor Green } 'DEBUG' { Write-Host $logEntry -ForegroundColor Cyan } 'PROGRESS'{ Write-Host $logEntry -ForegroundColor Magenta } default { Write-Host $logEntry } } } } # Progress logging function function Write-ProgressLog { param([string]$Message, [string]$Component = 'Progress') Write-Log -Message $Message -Level 'PROGRESS' -Component $Component } # Check if running as administrator function Test-Admin { $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # Initialize script function Initialize-Script { $script:LogFile = Initialize-LogPath Write-Log "========================================" -Level 'INFO' Write-Log "Accounts Security Options Configuration Started" -Level 'INFO' Write-Log "Script: $($MyInvocation.ScriptName)" -Level 'INFO' Write-Log "Log Level: $LogLevel" -Level 'INFO' Write-Log "WhatIf Mode: $WhatIf" -Level 'INFO' Write-Log "========================================" -Level 'INFO' if (-not (Test-Admin)) { Write-Log "ERROR: This script requires administrator privileges" -Level 'ERROR' exit 1 } } # Import INF file into structured data function Import-InfFile { param([string]$Path) if (-not (Test-Path $Path)) { Write-Log "INF file not found: $Path" -Level 'ERROR' return $null } $infData = @{} $currentSection = $null Get-Content $Path -Encoding Unicode | ForEach-Object { $line = $_.Trim() # Skip empty lines and comments if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith(';')) { return } # Section header if ($line -match '^\[(.+)\]$') { $currentSection = $matches[1] if (-not $infData.ContainsKey($currentSection)) { $infData[$currentSection] = @{} } return } # Key-value pair if ($currentSection -and $line -match '^(.+?)\s*=\s*(.*)$') { $key = $matches[1].Trim() $value = $matches[2].Trim() $infData[$currentSection][$key] = $value } } return $infData } # Write INF data back to file function Write-InfFile { param( [hashtable]$Data, [string]$Path ) $output = @() $output += "[Unicode]" $output += "Unicode=yes" # Ensure [Version] section is first if ($Data.ContainsKey('Version')) { $output += "[Version]" foreach ($key in $Data['Version'].Keys) { $output += "$key=$($Data['Version'][$key])" } } # Write other sections foreach ($section in ($Data.Keys | Where-Object { $_ -notin @('Unicode','Version') } | Sort-Object)) { $output += "[$section]" foreach ($key in ($Data[$section].Keys | Sort-Object)) { $output += "$key=$($Data[$section][$key])" } } $output | Out-File -FilePath $Path -Encoding unicode -Force } # Set individual secpol row function Set-SecpolRow { param( [string]$Name, [string]$KeyGroup, [string]$Key, [string]$Value, [string]$RegistryType, [hashtable]$PolicyData ) if ([string]::IsNullOrWhiteSpace($Value)) { Write-Log "Skipping '$Name' - No value provided" -Level 'WARNING' -Component $Name $script:SkippedCount++ return $false } # Special handling for string values (RegistryType = 1 or 7): Convert newlines to commas for INF format compatibility if ($RegistryType -eq "1" -or $RegistryType -eq "7") { if ($Value -match '(\r\n|\n|\r)') { # Replace various newline formats with commas $Value = $Value -replace '(\r\n|\n|\r)', ',' # Remove any trailing commas $Value = $Value.TrimEnd(',') Write-Log "Converted multi-line text to comma-separated format for INF compatibility" -Level 'DEBUG' -Component $Name } } Write-ProgressLog "Processing: $Name" -Component $Name # Format value for registry entries $finalValue = $Value if ($KeyGroup -eq "[Registry Values]" -and -not [string]::IsNullOrWhiteSpace($RegistryType)) { $finalValue = "$RegistryType,$Value" Write-Log "Setting: $Name = $finalValue (Type: $RegistryType)" -Level 'INFO' -Component $Name } else { Write-Log "Setting: $Name = $Value" -Level 'INFO' -Component $Name } Write-Log "Location: $KeyGroup\$Key" -Level 'DEBUG' -Component $Name if ($WhatIf) { Write-Log "[WhatIf] Would set $KeyGroup\$Key = $finalValue" -Level 'INFO' -Component $Name $script:SuccessCount++ return $true } try { # Update policy data $sectionName = $KeyGroup.Trim('[',']') if (-not $PolicyData.ContainsKey($sectionName)) { $PolicyData[$sectionName] = @{} } $PolicyData[$sectionName][$Key] = $finalValue Write-Log "Successfully configured: $Name" -Level 'SUCCESS' -Component $Name $script:SuccessCount++ return $true } catch { Write-Log "Failed to configure '$Name': $($_.Exception.Message)" -Level 'ERROR' -Component $Name $script:FailureCount++ return $false } } # Save all changes to security policy # Parse PolicyValues array string to array function Parse-PolicyValuesArray { param([string]$ArrayString) # Check if input is JSON format (starts with [ and ends with ]) if ($ArrayString -match '^\s*\[.*\]\s*$') { Write-Log "Detected JSON format input, attempting to parse..." -Level Debug try { Write-Log "Raw PolicyValues input: $ArrayString" -Level Debug # Fix malformed JSON: add quotes around unquoted alphanumeric values # Pattern 1: Handle unquoted values in the middle or at the end (e.g., ,Admin, or ,Admin]) $fixedString = $ArrayString -replace ',\s*([A-Za-z_][A-Za-z0-9_]*)\s*([,\]])', ',"$1"$2' # Pattern 2: Handle unquoted values at the beginning (e.g., [Admin,) $fixedString = $fixedString -replace '^\[\s*([A-Za-z_][A-Za-z0-9_]*)\s*,', '["$1",' if ($fixedString -ne $ArrayString) { Write-Log "Fixed malformed JSON - Original: $ArrayString" -Level Debug Write-Log "Fixed malformed JSON - Corrected: $fixedString" -Level Debug } # Parse the JSON string $Arguments = ConvertFrom-Json $fixedString Write-Log "Successfully parsed JSON policy values array: $($Arguments.Count) values provided" -Level Info return $Arguments } catch { Write-Log "Failed to parse as JSON: $($_.Exception.Message)" -Level Warning Write-Log "Falling back to positional argument parsing..." -Level Info } } else { Write-Log "Input is not in JSON format (doesn't start with [ ), using as positional arguments" -Level Info } # Fallback: Use PolicyValuesArray as positional arguments Write-Log "Using $($PolicyValuesArray.Count) positional arguments" -Level Info return $PolicyValuesArray } Write-Log "Arguments provided: $PolicyValues" # Parse PolicyValues array string to get individual arguments $Arguments = Parse-PolicyValuesArray -ArrayString $PolicyValues function Save-SecpolChanges { param([hashtable]$PolicyData) $exportPath = Join-Path $PSScriptRoot "secpol_export.inf" $modifiedPath = Join-Path $PSScriptRoot "secpol_modified.inf" try { Write-ProgressLog "Saving changes to security policy database..." # Write modified policy Write-InfFile -Data $PolicyData -Path $modifiedPath Write-Log "Modified policy written to: $modifiedPath" -Level 'DEBUG' # Apply the policy Write-Log "Applying security policy configuration..." -Level 'INFO' $seceditOutput = secedit /configure /db secedit.sdb /cfg $modifiedPath /areas SECURITYPOLICY 2>&1 if ($LASTEXITCODE -eq 0) { Write-Log "Security policy applied successfully" -Level 'SUCCESS' return $true } else { Write-Log "Secedit returned exit code: $LASTEXITCODE" -Level 'ERROR' Write-Log "Secedit output: $seceditOutput" -Level 'ERROR' return $false } } catch { Write-Log "Error saving security policy: $($_.Exception.Message)" -Level 'ERROR' return $false } } # Main execution Initialize-Script try { if ($PolicyDatabase.Count -eq 0) { Write-Log "Policy database is empty. Please populate the PolicyDatabase array." -Level 'WARNING' exit 0 } # Export current policy $exportPath = Join-Path $PSScriptRoot "secpol_export.inf" Write-Log "Exporting current security policy..." -Level 'INFO' secedit /export /cfg $exportPath /areas SECURITYPOLICY | Out-Null # Import current policy data $policyData = Import-InfFile -Path $exportPath if (-not $policyData) { Write-Log "Failed to import current security policy" -Level 'ERROR' exit 1 } # Process each policy with its corresponding argument for ($i = 0; $i -lt $PolicyDatabase.Count; $i++) { $policy = $PolicyDatabase[$i] $value = if ($i -lt $Arguments.Count) { $Arguments[$i] } else { $null } $script:ProcessedCount++ $regType = if ($policy.ContainsKey('RegistryType')) { $policy.RegistryType } else { $null } Set-SecpolRow -Name $policy.Name -KeyGroup $policy.KeyGroup -Key $policy.Key -Value $value -RegistryType $regType -PolicyData $policyData } # Save all changes if not in WhatIf mode and there were successful changes if (-not $WhatIf -and $script:SuccessCount -gt 0) { Save-SecpolChanges -PolicyData $policyData } } catch { Write-Log "Critical error in main execution: $($_.Exception.Message)" -Level 'ERROR' Write-Log "Stack trace: $($_.ScriptStackTrace)" -Level 'DEBUG' } finally { # Summary $duration = (Get-Date) - $script:StartTime Write-Log "========================================" -Level 'INFO' Write-Log "Execution Summary:" -Level 'INFO' Write-Log " Total Processed: $($script:ProcessedCount)" -Level 'INFO' Write-Log " Successful: $($script:SuccessCount)" -Level 'INFO' Write-Log " Failed: $($script:FailureCount)" -Level 'INFO' Write-Log " Skipped: $($script:SkippedCount)" -Level 'INFO' Write-Log " Duration: $($duration.TotalSeconds) seconds" -Level 'INFO' Write-Log "========================================" -Level 'INFO' }